Stock Market Trend Forecast

Using Technical Analysis and Machine Learning

Stock
Technical Analysis
Machine Learning
Author

Hoang Son Lai

Published

January 23, 2026

Code
# Load libraries
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from pandas.plotting import table
import os
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.io as pio
pio.renderers.default = "notebook_connected"
pio.templates.default = "plotly_white"
import pandas as pd
import pandas_ta as ta
import mplfinance as mpf
from xgboost import XGBRegressor
from sklearn.metrics import mean_squared_error
import ipywidgets as widgets
from IPython.display import display, clear_output
# To convert to html use quarto render ml_report.ipynb

# Load Data
try:
    df = pd.read_csv('../../data/cleaned/stock_prices.csv')
    df['date'] = pd.to_datetime(df['date'])
    # Sort by Ticker and Date for accurate calculation
    df = df.sort_values(['ticker', 'date'])
except FileNotFoundError:
    print("Error: File 'stock_prices.csv' not found.")

Part 1. Technical Analysis

This section provides a comprehensive technical analysis of stock prices using multiple indicators. For each stock, I calculate key metrics:

  1. Trend Indicators:
  • MA50/MA200: 50-day and 200-day Simple Moving Averages compare current price to medium/long-term trends (Price > MA = bullish +1, Price < MA = bearish -1)

  • EMA20: 20-day Exponential Moving Average gives more weight to recent prices for short-term trend direction

  1. Momentum Indicators:
  • RSI (14-day): Measures overbought (>70 = -1) vs. oversold (<30 = +1) conditions, with 30-70 being neutral

  • MACD: Signal line crossover indicator (MACD > 0 = +1 bullish momentum, MACD < 0 = -1 bearish momentum)

  1. Volatility & Volume Indicators:
  • Bollinger Bands: Price above upper band = overbought (-1), below lower band = oversold (+1), within bands = neutral (0)

  • MFI (Money Flow Index): Volume-weighted RSI (>80 = -1 overbought, <20 = +1 oversold, 20-80 = neutral)

Each indicator is scored (+1 for bullish, -1 for bearish, 0 for neutral), with a final aggregate signal determining the overall market outlook (Positive, Negative, or Neutral).

Code
# 1. Define a function to calculate indicators for a single dataframe
def calculate_indicators(data):
    # Ensure data is sorted by date
    data = data.sort_values('date').reset_index(drop=True)
    
    # Trend Indicators
    data.ta.sma(length=50, append=True)  # Simple Moving Average (50)
    data.ta.sma(length=200, append=True) # Simple Moving Average (200)
    data.ta.ema(length=20, append=True)  # Exponential Moving Average (20)
    
    # Momentum Indicators
    data.ta.rsi(length=14, append=True)  # Relative Strength Index (14)
    
    # Volatility Indicators - Bollinger Bands
    # Using bbands with standard parameters
    bbands = data.ta.bbands(length=20, std=2, append=True)
    
    # Trend Following (MACD)
    data.ta.macd(fast=12, slow=26, signal=9, append=True)
    
    # Money Flow Index (MFI)
    data.ta.mfi(length=14, append=True)
    
    # Volume
    data['Volume'] = data['volume']
    
    return data

# 2. Apply calculation to all tickers
# Create a list to store processed dataframes
processed_frames = []

# Get list of unique tickers
tickers = df['ticker'].unique()

for ticker in tickers:
    # Filter data for specific ticker
    ticker_df = df[df['ticker'] == ticker].copy()
    
    # Calculate indicators (need at least 200 days for MA200)
    if len(ticker_df) > 200:
        try:
            ticker_df = calculate_indicators(ticker_df)
            processed_frames.append(ticker_df)
            print(f"✓ Processed {ticker}: {len(ticker_df)} rows, columns: {len(ticker_df.columns)}")
        except Exception as e:
            print(f"✗ Error processing {ticker}: {e}")
    else:
        print(f"✗ Skipped {ticker}: Not enough data ({len(ticker_df)} rows, need at least 200)")

# Combine back into a single main DataFrame
if processed_frames:
    df_ta = pd.concat(processed_frames, ignore_index=True)
    print(f"\nCombined DataFrame shape: {df_ta.shape}")
    print(f"Available columns: {df_ta.columns.tolist()}")
else:
    print("No data was processed!")
    df_ta = pd.DataFrame()

# 3. Check column names and find Bollinger Bands columns
if not df_ta.empty:
    print("\nSearching for Bollinger Bands columns...")
    bollinger_cols = [col for col in df_ta.columns if 'BB' in col or 'bb' in col]
    print(f"Bollinger Bands columns found: {bollinger_cols}")
    
    # Find the actual column names for BB upper and lower
    bb_upper = None
    bb_lower = None
    bb_middle = None
    
    for col in bollinger_cols:
        col_lower = col.lower()
        if 'bbu' in col_lower or 'upper' in col_lower:
            bb_upper = col
        elif 'bbl' in col_lower or 'lower' in col_lower:
            bb_lower = col
        elif 'bbm' in col_lower or 'middle' in col_lower:
            bb_middle = col
    
    print(f"Upper BB column: {bb_upper}")
    print(f"Lower BB column: {bb_lower}")
    print(f"Middle BB column: {bb_middle}")

# 4. Calculate scores for each indicator with safe column access
def calculate_scores(row):
    scores = {}
    
    # Helper function to safely get values
    def get_value(col_name, default=np.nan):
        if col_name in row and pd.notna(row[col_name]):
            return row[col_name]
        return default
    
    # MA50 vs Close
    ma50 = get_value('SMA_50')
    if pd.notna(ma50):
        if row['close'] > ma50:
            scores['MA50'] = 1
        elif row['close'] < ma50:
            scores['MA50'] = -1
        else:
            scores['MA50'] = 0
    
    # MA200 vs Close
    ma200 = get_value('SMA_200')
    if pd.notna(ma200):
        if row['close'] > ma200:
            scores['MA200'] = 1
        elif row['close'] < ma200:
            scores['MA200'] = -1
        else:
            scores['MA200'] = 0
    
    # EMA20 vs Close
    ema20 = get_value('EMA_20')
    if pd.notna(ema20):
        if row['close'] > ema20:
            scores['EMA'] = 1
        elif row['close'] < ema20:
            scores['EMA'] = -1
        else:
            scores['EMA'] = 0
    
    # MACD
    macd = get_value('MACD_12_26_9') or get_value('MACD')
    if pd.notna(macd):
        if macd > 0:
            scores['MACD'] = 1
        elif macd < 0:
            scores['MACD'] = -1
        else:
            scores['MACD'] = 0
    
    # RSI
    rsi = get_value('RSI_14')
    if pd.notna(rsi):
        if rsi > 70:
            scores['RSI'] = -1  # Overbought
        elif rsi < 30:
            scores['RSI'] = 1   # Oversold
        else:
            scores['RSI'] = 0   # Neutral
    
    # Bollinger Bands
    if bb_upper and bb_lower:
        bb_upper_val = get_value(bb_upper)
        bb_lower_val = get_value(bb_lower)
        
        if pd.notna(bb_upper_val) and pd.notna(bb_lower_val):
            if row['close'] > bb_upper_val:
                scores['BB'] = -1   # Overbought (above upper band)
            elif row['close'] < bb_lower_val:
                scores['BB'] = 1    # Oversold (below lower band)
            else:
                scores['BB'] = 0    # Within bands
    
    # MFI (Money Flow Index)
    mfi = get_value('MFI_14')
    if pd.notna(mfi):
        if mfi > 80:
            scores['MFI'] = -1  # Overbought
        elif mfi < 20:
            scores['MFI'] = 1   # Oversold
        else:
            scores['MFI'] = 0   # Neutral
    
    # Calculate total score
    total_score = sum(scores.values()) if scores else 0
    
    # Determine signal
    if total_score > 0:
        signal = 'Positive'
    elif total_score < 0:
        signal = 'Negative'
    else:
        signal = 'Neutral'
    
    # Add scores and signal to row
    row['MA50_Score'] = scores.get('MA50', 0)
    row['MA200_Score'] = scores.get('MA200', 0)
    row['EMA_Score'] = scores.get('EMA', 0)
    row['MACD_Score'] = scores.get('MACD', 0)
    row['RSI_Score'] = scores.get('RSI', 0)
    row['BB_Score'] = scores.get('BB', 0)
    row['MFI_Score'] = scores.get('MFI', 0)
    row['Total_Score'] = total_score
    row['Signal'] = signal
    
    return row

# Apply scoring function if we have data
if not df_ta.empty:
    print("\nCalculating scores for each row...")
    df_ta = df_ta.apply(calculate_scores, axis=1)
    
    # 5. Create a summary report table (latest data for each ticker)
    # Get the latest date for each ticker
    latest_dates = df_ta.groupby('ticker')['date'].max()
    
    # Create summary dataframe
    summary_rows = []
    for ticker, latest_date in latest_dates.items():
        ticker_data = df_ta[(df_ta['ticker'] == ticker) & (df_ta['date'] == latest_date)]
        if not ticker_data.empty:
            latest_row = ticker_data.iloc[0]
            
            summary_rows.append({
                'Ticker': ticker,
                'Close Price': latest_row['close'],
                'Volume': latest_row['volume'],
                'MA50': latest_row.get('SMA_50', np.nan),
                'MA200': latest_row.get('SMA_200', np.nan),
                'EMA20': latest_row.get('EMA_20', np.nan),
                'MACD': latest_row.get('MACD_12_26_9', np.nan),
                'RSI': latest_row.get('RSI_14', np.nan),
                'BB Upper': latest_row.get(bb_upper, np.nan) if bb_upper else np.nan,
                'BB Lower': latest_row.get(bb_lower, np.nan) if bb_lower else np.nan,
                'MFI': latest_row.get('MFI_14', np.nan),
                'MA50 Score': latest_row['MA50_Score'],
                'MA200 Score': latest_row['MA200_Score'],
                'EMA Score': latest_row['EMA_Score'],
                'MACD Score': latest_row['MACD_Score'],
                'RSI Score': latest_row['RSI_Score'],
                'BB Score': latest_row['BB_Score'],
                'MFI Score': latest_row['MFI_Score'],
                'Total Score': latest_row['Total_Score'],
                'Signal': latest_row['Signal']
            })
    
    # Create summary dataframe
    summary_df = pd.DataFrame(summary_rows)
    
    # 6. Display the summary table with styling
    def style_scores(val):
        if isinstance(val, (int, float)):
            if val > 0:
                return 'color: green; font-weight: bold;'
            elif val < 0:
                return 'color: red; font-weight: bold;'
            else:
                return 'color: black;'
        return ''
    
    def style_signal(val):
        if val == 'Positive':
            return 'background-color: #d4edda; color: #155724; font-weight: bold;'
        elif val == 'Negative':
            return 'background-color: #f8d7da; color: #721c24; font-weight: bold;'
        else:
            return 'background-color: #fff3cd; color: #856404; font-weight: bold;'
    
    # Apply styling - using map() instead of applymap() for newer pandas versions
    score_columns = ['MA50 Score', 'MA200 Score', 'EMA Score', 'MACD Score', 
                    'RSI Score', 'BB Score', 'MFI Score', 'Total Score']
    
    # Filter columns that actually exist in the dataframe
    existing_score_cols = [col for col in score_columns if col in summary_df.columns]
    
    # Create styler object
    styler = summary_df.style
    
    # Apply formatting
    styler = styler.format({
        'Close Price': '{:.2f}',
        'MA50': '{:.2f}',
        'MA200': '{:.2f}',
        'EMA20': '{:.2f}',
        'MACD': '{:.4f}',
        'RSI': '{:.1f}',
        'BB Upper': '{:.2f}',
        'BB Lower': '{:.2f}',
        'MFI': '{:.1f}',
        'Volume': '{:,.0f}'
    })
    
    # Apply styling to score columns (use map for newer pandas)
    if existing_score_cols:
        styler = styler.map(style_scores, subset=existing_score_cols)
    
    # Apply styling to signal column
    if 'Signal' in summary_df.columns:
        styler = styler.map(style_signal, subset=['Signal'])
    
    # Set properties and table styles
    styler = styler.set_properties(**{
        'text-align': 'center',
        'border': '1px solid #ddd',
        'padding': '5px'
    })
    
    styler = styler.set_caption("Technical Analysis Summary - Latest Signals")
Code
# Display the styled table
styler = (
    styler
    .set_table_attributes('style="width:100%; border-collapse: collapse;"')
    .set_table_styles([
        # table scroll
        {
            "selector": "table",
            "props": [
                ("display", "block"),
                ("overflow-x", "auto"),
                ("white-space", "nowrap")
            ]
        },

        # freeze 1
        {
            "selector": "th:nth-child(1), td:nth-child(1)",
            "props": [
                ("position", "sticky"),
                ("left", "0px"),
                ("background", "white"),
                ("z-index", "3")
            ]
        },

        # freeze 2
        {
            "selector": "th:nth-child(2), td:nth-child(2)",
            "props": [
                ("position", "sticky"),
                ("left", "20px"),   
                ("background", "white"),
                ("z-index", "3")
            ]
        },

        # header
        {
            "selector": "thead th",
            "props": [
                ("background-color", "#4a6fa5"),
                ("color", "white"),
                ("font-weight", "bold"),
                ("padding", "10px"),
                ("border", "1px solid #ddd"),
                ("position", "sticky"),
                ("top", "0"),
                ("z-index", "4")
            ]
        },
        
        {
            "selector": "thead th:nth-child(1), thead th:nth-child(2)",
            "props": [
              ("background-color", "#4a6fa5"),
              ("z-index", "5")
            ]
        },
        
        # body cell
        {
            "selector": "tbody td",
            "props": [
                ("padding", "8px"),
                ("border", "1px solid #ddd")
            ]
        },

        # hover
        {
            "selector": "tr:hover",
            "props": [
                ("background-color", "#f5f5f5")
            ]
        }
    ])
)

display(styler)
Table 1: Technical Analysis Summary - Latest Signals
  Ticker Close Price Volume MA50 MA200 EMA20 MACD RSI BB Upper BB Lower MFI MA50 Score MA200 Score EMA Score MACD Score RSI Score BB Score MFI Score Total Score Signal
0 AAPL 248.68 20,369,381 269.78 234.74 259.27 -6.1921 23.5 280.05 243.12 25.3 -1 1 -1 -1 1 0 0 -1 Negative
1 ADBE 302.73 1,708,290 331.50 357.65 318.17 -12.2318 34.1 370.32 280.05 32.9 -1 -1 -1 -1 0 0 0 -4 Negative
2 AMZN 239.68 17,267,451 232.37 220.30 236.04 1.4068 55.5 248.81 224.73 61.6 1 1 1 1 0 0 0 4 Positive
3 BAC 51.58 14,331,746 53.90 48.30 53.79 -0.6639 34.6 58.25 50.93 45.3 -1 1 -1 -1 0 0 0 -2 Negative
4 DIS 111.56 4,051,824 109.76 110.60 112.44 0.4457 48.3 115.97 110.68 59.2 1 1 -1 1 0 0 0 2 Positive
5 GOOGL 328.39 12,237,364 312.72 229.67 323.91 5.7880 58.3 340.02 306.56 58.9 1 1 1 1 0 0 0 4 Positive
6 HD 383.07 996,841 356.24 370.44 368.76 7.9576 65.6 395.88 331.25 75.4 1 1 1 1 0 0 0 4 Positive
7 JNJ 218.29 2,194,145 206.78 176.21 212.80 3.7928 67.7 222.94 199.47 60.9 1 1 1 1 0 0 0 4 Positive
8 JPM 297.76 4,505,715 312.95 288.98 313.03 -3.7925 34.8 341.22 295.53 45.1 -1 1 -1 -1 0 0 0 -2 Negative
9 KO 72.26 5,750,665 70.56 69.11 70.70 0.4989 62.9 72.96 67.49 62.4 1 1 1 1 0 0 0 4 Positive
10 MA 525.59 1,739,901 553.92 561.10 550.38 -8.6462 30.9 599.35 518.87 35.0 -1 -1 -1 -1 0 0 0 -4 Negative
11 META 661.78 11,566,994 639.49 676.81 640.07 -5.2857 57.2 684.18 604.76 56.3 1 -1 1 -1 0 0 0 0 Neutral
12 MSFT 469.30 19,925,313 480.94 482.82 468.51 -7.9125 48.3 499.12 445.07 42.7 -1 -1 1 -1 0 0 0 -2 Negative
13 NFLX 86.00 34,573,696 97.70 113.01 89.75 -3.3278 31.6 96.17 83.84 20.0 -1 -1 -1 -1 0 0 0 -4 Negative
14 NVDA 187.94 80,231,943 183.83 165.69 184.93 0.1661 54.3 191.82 180.78 46.7 1 1 1 1 0 0 0 4 Positive
15 PG 150.58 6,517,631 144.97 153.20 145.17 1.0609 64.9 150.24 137.90 66.2 1 -1 1 1 0 -1 0 1 Positive
16 PYPL 56.91 4,045,483 60.13 67.19 57.83 -1.2061 42.1 60.75 55.14 35.2 -1 -1 -1 -1 0 0 0 -4 Negative
17 TSLA 448.26 34,982,226 442.38 372.75 444.51 -3.4541 51.3 476.34 415.56 55.6 1 1 1 -1 0 0 0 2 Positive
18 UNH 355.18 3,106,433 331.31 332.55 340.37 4.3885 63.2 355.84 322.73 58.8 1 1 1 1 0 0 0 4 Positive
19 V 327.00 2,249,084 338.03 344.07 336.65 -4.8379 35.0 368.38 316.18 40.4 -1 -1 -1 -1 0 0 0 -4 Negative
20 WMT 118.40 8,188,390 112.18 102.04 116.44 1.9834 60.2 122.34 108.75 27.9 1 1 1 1 0 0 0 4 Positive

Part 2. Machine Learning

1. Overview

Building upon the technical analysis in Part 1, this section utilizes Machine Learning (XGBoost) to predict stock prices. Unlike traditional indicators that give simple Buy/Sell signals, the ML model analyzes the complex relationships between historical patterns (RSI, MACD, Moving Averages) to forecast the exact closing price of the next trading day.

2. Methodology

  • Algorithm: We use XGBRegressor (Extreme Gradient Boosting), a robust algorithm highly effective for structured time-series data.
  • Feature Engineering: The model inputs include Open, High, Low, Volume, and all technical indicators calculated in Part 1 (SMA, EMA, Bollinger Bands, etc.).
  • Training & Validation: To prevent “data leakage” (looking into the future), the data is split chronologically:
    • Training Set (First 80%): Used to teach the model historical patterns.
    • Test Set (Last 20%): Used to evaluate how well the model predicts unseen data.

3. Interactive Analysis Dashboard

The visualization below provides a comprehensive view of the model’s performance. Use the dropdown menu to select a specific ticker: * Top Chart (Actual vs. Predicted): Compares the real market price (Blue line) against the model’s prediction (Orange dotted line). The closer the lines, the better the model accuracy. * Bottom Chart (Feature Importance): Ranks which technical indicators were most influential in determining the price. For example, if RSI has a high bar, the model relies heavily on momentum to make predictions for that specific stock.

Note: The table below summarizes the prediction for the next upcoming trading day, including the predicted percentage change.

Code
# ==========================================
# Part 2. Machine Learning Prediction
# ==========================================

# 1. Feature Engineering & Data Preparation
# ------------------------------------------
# Identify feature columns (Technical Indicators + Price data)
# We exclude non-numeric columns and the 'Score' columns created in Part 1 logic
exclude_cols = ['ticker', 'date', 'adj_close', 'Signal', 'Volume']
feature_cols = ['open', 'high', 'low', 'close', 'volume']

# Add technical indicator columns created by pandas_ta (usually uppercase)
# We filter out the 'Score' columns we made manually in Part 1
indicator_cols = [c for c in df_ta.columns 
                  if c not in feature_cols + exclude_cols 
                  and 'Score' not in c 
                  and 'Total_Score' not in c]

features = feature_cols + indicator_cols


# Dictionary to store results and models
ml_results = []
ticker_predictions = {}
models = {}


# 2. Training Loop
# ------------------------------------------
for ticker in df_ta['ticker'].unique():
    # Prepare data for this ticker
    df_ticker = df_ta[df_ta['ticker'] == ticker].copy()
    
    # Sort by date ensures time-series integrity
    df_ticker = df_ticker.sort_values('date')
    
    # Create Target: Predict Next Day's Close Price
    # We shift the 'close' price backwards by 1 day
    df_ticker['Target'] = df_ticker['close'].shift(-1)
    
    # Create a cleaner dataset for ML (drop NaNs from indicators and shifting)
    # meaningful_data includes the latest row (which has features but NaN target)
    meaningful_data = df_ticker[features].copy()
    
    # Valid data for training (must have both Features and Target)
    train_data = df_ticker.dropna(subset=features + ['Target'])
    
    if len(train_data) < 100:
        print(f"Skipping {ticker}: Not enough data points ({len(train_data)})")
        continue
        
    # Split Data: Time-Series Split (No random shuffling!)
    # Use first 80% for training, last 20% for testing
    split_idx = int(len(train_data) * 0.8)
    
    X = train_data[features]
    y = train_data['Target']
    
    X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:]
    y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:]
    
    # Train Model (XGBoost)
    model = XGBRegressor(
        objective='reg:squarederror',
        n_estimators=100,
        learning_rate=0.05,
        max_depth=5,
        random_state=42,
        n_jobs=-1
    )
    
    model.fit(X_train, y_train)
    models[ticker] = model
    
    # Evaluate on Test Set
    preds = model.predict(X_test)
    rmse = np.sqrt(mean_squared_error(y_test, preds))
    mae = np.mean(np.abs(y_test - preds))
    
    # Store test set predictions for visualization
    test_dates = df_ticker.loc[X_test.index, 'date']
    ticker_predictions[ticker] = pd.DataFrame({
        'date': test_dates,
        'Actual': y_test,
        'Predicted': preds
    })
    
    # 3. Future Prediction (Predict Next Day)
    # ------------------------------------------
    # Get the very last row of data (today/latest available)
    last_row = df_ticker.iloc[[-1]][features]
    
    # Predict
    future_pred = model.predict(last_row)[0]
    current_price = df_ticker.iloc[-1]['close']
    predicted_change = ((future_pred - current_price) / current_price) * 100
    
    ml_results.append({
        'Ticker': ticker,
        'Current Date': df_ticker.iloc[-1]['date'],
        'Current Price': current_price,
        'Predicted Next Price': future_pred,
        'Predicted Change %': predicted_change,
        'RMSE (Error)': rmse
    })
    
Code
# 4. Results Summary
# ------------------------------------------
df_ml_results = pd.DataFrame(ml_results)

# Styling the output
def style_ml_results(val):
    if isinstance(val, float):
        if val > 0.5: # Predicted increase > 0.5%
            return 'color: green; font-weight: bold'
        elif val < -0.5: # Predicted decrease > 0.5%
            return 'color: red; font-weight: bold'
    return ''

display(df_ml_results.style.format({
    'Current Price': '{:.2f}',
    'Predicted Next Price': '{:.2f}',
    'Predicted Change %': '{:+.2f}%',
    'RMSE (Error)': '{:.2f}'
}).map(style_ml_results, subset=['Predicted Change %'])
  .set_caption("Machine Learning Price Predictions (Next Trading Day)"))


# 5. Visualization Dashboard
# Create a single Figure with Subplots
fig = make_subplots(
    rows=2, cols=1,
    row_heights=[0.7, 0.3],
    vertical_spacing=0.15,
    subplot_titles=("Price Prediction (Test Set)", "Feature Importance"),
    specs=[[{"type": "scatter"}], [{"type": "bar"}]] # Define types explicitly
)

tickers_list = list(ticker_predictions.keys())
num_tickers = len(tickers_list)

# We will add ALL traces to the figure, but set visible=False for most
# Each ticker adds 3 traces: Actual (Line), Predicted (Line), Importance (Bar)
traces_per_ticker = 3 

for i, ticker in enumerate(tickers_list):
    # Visibility: Only True for the first ticker
    is_visible = (i == 0)
    
    # 1. Actual Price Trace
    pred_df = ticker_predictions[ticker]
    fig.add_trace(
        go.Scatter(
            x=pred_df['date'], 
            y=pred_df['Actual'],
            mode='lines', 
            name=f'Actual',
            line=dict(color='#1f77b4', width=2),
            visible=is_visible,
            legendgroup=f"group_{ticker}"
        ),
        row=1, col=1
    )
    
    # 2. Predicted Price Trace
    fig.add_trace(
        go.Scatter(
            x=pred_df['date'], 
            y=pred_df['Predicted'],
            mode='lines', 
            name=f'Predicted',
            line=dict(color='#ff7f0e', width=2, dash='dot'),
            visible=is_visible,
            legendgroup=f"group_{ticker}"
        ),
        row=1, col=1
    )
    
    # 3. Feature Importance Trace
    model = models[ticker]
    importance = pd.DataFrame({
        'Feature': features,
        'Importance': model.feature_importances_
    }).sort_values('Importance', ascending=True).tail(10)
    
    fig.add_trace(
        go.Bar(
            x=importance['Importance'], 
            y=importance['Feature'],
            orientation='h', 
            name=f'Importance',
            marker_color='#2ca02c',
            visible=is_visible,
            legendgroup=f"group_{ticker}"
        ),
        row=2, col=1
    )

# Create Dropdown Menu Steps
steps = []
for i, ticker in enumerate(tickers_list):
    # Create a visibility array: [False, False, ..., True, True, True, ..., False]
    # Only the 3 traces corresponding to this ticker should be True
    step = dict(
        method="update",
        args=[{"visible": [False] * (num_tickers * traces_per_ticker)},
              {"title": f"Analysis for: {ticker}"}],
        label=ticker
    )
    # Set the 3 traces for this ticker to True
    base_idx = i * traces_per_ticker
    step["args"][0]["visible"][base_idx] = True     # Actual
    step["args"][0]["visible"][base_idx + 1] = True # Predicted
    step["args"][0]["visible"][base_idx + 2] = True # Importance
    
    steps.append(step)

# Add Menu to Layout
sliders = [dict(
    active=0,
    currentvalue={"prefix": "Select Ticker: "},
    pad={"t": 50},
    steps=steps
)]

# Dropdown menu instead of slider for better UI
updatemenus = [dict(
    buttons=steps,
    direction="down",
    pad={"r": 10, "t": 10},
    showactive=True,
    x=0.1,
    xanchor="left",
    y=1.15,
    yanchor="top"
)]

fig.update_layout(
    height=800,
    updatemenus=updatemenus,
    showlegend=True,
    template="plotly_white",
    margin=dict(t=100) # Add margin for dropdown
)

# Explicitly show the figure
fig.show()
Table 2: Machine Learning Price Predictions (Next Trading Day)
  Ticker Current Date Current Price Predicted Next Price Predicted Change % RMSE (Error)
0 AAPL 2026-01-23 00:00:00 248.68 238.14 -4.24% 16.08
1 ADBE 2026-01-23 00:00:00 302.73 307.61 +1.61% 8.91
2 AMZN 2026-01-23 00:00:00 239.68 229.87 -4.10% 7.08
3 BAC 2026-01-23 00:00:00 51.58 44.43 -13.86% 5.63
4 DIS 2026-01-23 00:00:00 111.56 110.53 -0.93% 2.64
5 GOOGL 2026-01-23 00:00:00 328.39 190.51 -41.99% 67.94
6 HD 2026-01-23 00:00:00 383.07 386.27 +0.84% 6.52
7 JNJ 2026-01-23 00:00:00 218.29 160.07 -26.67% 25.67
8 JPM 2026-01-23 00:00:00 297.76 256.04 -14.01% 38.18
9 KO 2026-01-23 00:00:00 72.26 67.58 -6.48% 2.06
10 MA 2026-01-23 00:00:00 525.59 525.88 +0.06% 17.32
11 META 2026-01-23 00:00:00 661.78 654.78 -1.06% 52.80
12 MSFT 2026-01-23 00:00:00 469.30 421.86 -10.11% 64.64
13 NFLX 2026-01-23 00:00:00 86.00 85.90 -0.13% 18.52
14 NVDA 2026-01-23 00:00:00 187.94 131.26 -30.16% 42.09
15 PG 2026-01-23 00:00:00 150.58 151.98 +0.93% 2.75
16 PYPL 2026-01-23 00:00:00 56.91 57.30 +0.68% 1.63
17 TSLA 2026-01-23 00:00:00 448.26 409.19 -8.71% 30.66
18 UNH 2026-01-23 00:00:00 355.18 428.58 +20.66% 108.85
19 V 2026-01-23 00:00:00 327.00 331.72 +1.44% 8.17
20 WMT 2026-01-23 00:00:00 118.40 99.72 -15.77% 8.49